Chapter 10: Tuples
From book Python Programming (Problem solving, Packages and Libraries) published by McGraw Hill Education (India) Private limited. By:

  • Anurag Gupta
  • G. P. Biswas

Note the following:-

  1. This html document is meant as an accompaniment to Chapter 10 Tuples .
  2. The document contains scripts executed on IDLE as well as on Jupyter notebook.
  3. The scripts executed on Jupyter can be directly copied and run into a Jupyter notebook or some other IDE (Like Pycharm or Eclipse with PyDev or Visual studio).
  4. However the scripts on IDLE also contain the >>> symbol and therefore cannot be directly executed. If you want to execute them on IDLE or Jupyter, you need to manually remove the >>> symbol.
  5. Wherever needed some background material from the book is also included to help you better understand the scripts
  6. The topic numbers given on each paragraph match the topic numbers of the book, so you can easily identify the topics and corresponding scripts.
  7. In some of the scripts, the file paths give are that of the author's computer. You need to replace them with file paths of your own computer.
  8. At some places, to improve readability, page numbers of the book are indicated in green font like:- See Page 181 of the book
  9. This document was first created as a Jupyter Notebook as combination of Markdown and code cells (extension .ipynb) and then downloaded as html. If someone wants to "modify" or "extend' this document, you may ask for the original .ipynb file by sending me an e-mail at:- 999.anuraggupta@gmail.com

10.2.1. Immutability concept
Following code on IDLE explains the concepts:

# ---ON IDLE---
>>> t = 'a', 1, 'b', 2  # This is a tuple. Enclosing brackets not essential
>>> t2 = ('a', 1, 'b', 2) # But brackets improve readability of a tuple
>>> type(t)
<class'tuple'>
>>> type(t2)
<class'tuple'>

However, if a tuple consisting of a single element is to be created, there must be a trailing comma otherwise the Python interpreter will treat it like a string. This will be clear from the following:

# ---ON IDLE---
>>> seq1 = 'a'# This will be treated as a string not a tuple
>>> seq2 = 'a', # This will be treated like a tuple because of the trailing comma
>>> type(seq1)
<class'str'>
>>> type(seq2)
<class'tuple'>
>>> t = 1,
>>> t + (60,) # Note (60,) is a tuple
(1, 60)

10.2.2. Creating, initializing and accessing elements

  1. Creating an empty tuple:

You can create an empty tuple using () or tuple() as shown:

# ---ON IDLE---
>>> t1 = () # Creates an empty tuple
>>> t2 = tuple() # Also creates an empty tuple
>>> t1
()
>>> t2
()

3. Creating a tuple from an iterable
The general syntax for creating a dictionary from an iterable object is as follows:

# ---ON IDLE---
tuple(iterable_object)

where iterable_object is a Python object, which can be iterated upon. Therefore, this means that a tuple can be created from a string, a list or a dictionary, as shown:

# ---ON IDLE---
>>> d1 = tuple('abcd') # Create tuple from a string
>>> d1
('a', 'b', 'c', 'd')
>>> d2 = tuple(['a', 'b', 'c', 'd']) # Create tuple from a list
>>> d2
('a', 'b', 'c', 'd')
>>> d3 = tuple({'a':'apple',  'b':'baby', 'c': 'cat', 'd': 'dog'}) #from dict
>>> d3 # Only keys of the dictionary are taken in the tuple.
('c', 'd', 'b', 'a')

10.2.3. Accessing items in a tuple and creating new tuples from existing
It is possible to access (but not modify) the individual items of a tuple. The format is just like in other sequences (Strings and lists), that is, one uses ‘square brackets with index’. Similarly, it is possible to create a new tuple by taking a slice or part of an existing tuple. (Note you cannot slice a tuple but you can assign a part of a tuple to a new variable, thereby creating a new tuple), as shown in the following example:

# ---ON IDLE---
>>> t = ('a', 'b', 'c', 'd')
>>> t[0] # Access the element at index 9 of the tuple t
'a'
>>> t1 = t[1:3] # Create a new tuple by taking a part of existing tuple
>>> t1
('b', 'c')
>>> t # The original tuple t does not change
('a', 'b', 'c', 'd')

10.2.5. Creating a tuple from user input (Using + operator)
The following script shows how users can be asked to give input numbers and store them in a tuple.

In [1]:
t = tuple()	
n = int(input("Number of numbers to be stored-> "))
for x in range(n):
    y = float(input("Give a number-> "))# input is a string so is cast to float
    t = t + (y,)# Can use ‘+’ operator to concatenate tuples
print(t)
Number of numbers to be stored-> 3
Give a number-> 12
Give a number-> 13
Give a number-> 14
(12.0, 13.0, 14.0)

10.2.6. ‘Immutability’ versus ‘reassignment’
A common confusion which arises in minds of the beginners is immutability versus reassignment. Immutability prohibits you to change individual members of a tuple by assigning new values to them after value has already been assigned. For instance, following is not allowed:

# ---ON IDLE---
>>> t1 = ('a', 'b', 'c')
>>> t1[0]# OK. You can “access” individual members of a tuple
'a'
>>> t1[0] = 'x'# Error. Cannot change individual members of a tuple
Traceback (most recent call last):
  File "<pyshell#16>", line 1, in<module>
    t1[0] = 'x'
TypeError: 'tuple' object does not support item assignment
>>>

However, you can always reassign a different tuple to the same variable name. For instance, look at the following:

# ---ON IDLE---
>>> t1 = ('a', 'b', 'c')
>>> t2 = ('x', 'y', 'z')
>>> t1 = t1 + t2 # Here t1 is not mutated but reassigning to different tuple
>>> t1
('a', 'b', 'c', 'x', 'y', 'z')
>>>

10.3.1. Operations
Some common operations on tuples are as follows:

  • Getting length of a tuple t1 using len(t1)
  • Concatenation of two tuples t1 and t2 by t1 + t2.
  • Multiplication of a tuple t1 with a positive integer say n using t1 n or n t1.
  • Membership operator, val in t1 (Here, val is a possible item in the tuple t1. If val exists in t1, this evaluates to True, else it is False).
  • Using syntax for var in t1 (Where var is a local variable and t1 is a tuple. Used to iterate over the items of a tuple).
    Since many of the common tuple operations are common to other sequences, they don’t need detailed explanation. They can be understood by the following examples:
    # ---ON IDLE---
    >>> len(('a', 'b', 'c', 'd')) # len() function
    4
    >>> ('a', 'b') + ('c', 'd') #'+' concatenates two tuples -> ('a', 'b', 'c', 'd')
    ('a', 'b', 'c', 'd')
    >>> ('abc',) * 5# '*' operator with int n=5-> ('abc', 'abc', 'abc', 'abc', 'abc')
    ('abc', 'abc', 'abc', 'abc', 'abc')
    >>>'c' in ('a', 'd', 'c', 'b') # Check if 'c' in the tuple
    True
    >>>for x in ('a', 'b', 'c'): print(x) # Tuple can be iterated upon in a for loop
    a
    b
    c
    
    One important point to be noted is that adding two tuples say t1 + t2 or multiplying a tuple with an integer say t1 * n does not modify t1 or t2. Rather, it creates new tuples. Further, these new tuples can also be assigned to new names or even old names as follows:
    # ---ON IDLE---
    >>> t1 = (1, 2)
    >>> t2 = ('three', 'four')
    >>> t3 = t1 + t2 # The RHS ie t1 + t2 creates new tuple which is assigned to t3
    >>> t3 
    (1, 2, 'three', 'four')
    >>> t2 = t1 + t2 # New tuple t1+t2 can be assigned to t2 also
    >>> t2
    (1, 2, 'three', 'four')
    >>> t1 = t1 * 3# Multiply t1 with 3 and reassign the new tuple to t1
    >>> t1
    (1, 2, 1, 2, 1, 2)
    

10.3.2. Some common tuple functions cmp(t1, t2) or use comparison operator ( t1 > t2, t1 < t2 or t1 == t2)
Note: The cmp(t1, t2) function is not available in Python 3.x. It is available in Python 2.x only. In Python 3.x use comparison operators, that is, <, >, or ==.
In Python, the comparison operator cmp() can be used to compare any two objects including tuples. The way the cmp() function works is as follows:

  • It first compares the first element of the two sequences.
  • If the first two elements are equal, it goes on to the next element and keeps on going till it finds two elements, which are not equal.
  • Once it finds two unequal elements, it compares them and accordingly gives result of True or False.
  • Note that once unequal elements are found, subsequent elements are not considered.

The following code on IDLE clarifies the concept:

# ---ON IDLE---
>>> (0,3,0) > (0, 1, 100) # Item at index 1 of first tuple greater than of second
True

2. max(t) (Where t is a tuple)
See Page 233 of the book
This function returns from the tuple the item with maximum value. [Note that for characters, the maximum value is determined by its ASCII code. Note that the ASCII of a character in Python, for instance, ‘a’, can be found using the inbuilt function ord(‘a’).]

# ---ON IDLE---
>>> t1 = (5,4,3,6,7,9,8)
>>> max(t1)
9
>>> t2 = ('a', 'A', 'b', 'B') # ASCII of A, a, B, b are 65, 97, 66 and 98. 
>>> max(t2) # For characters, the max(t) will return character with maximum ASCII
'b'

10.3.3. Swapping tuples
The traditional way of swapping the values or to be more precise the objects pointed to by two variable names, is to use a temporary variable name shown as follows:

# ---ON IDLE---
>>> n1 = 100
>>> n2 = 200
>>> temp = n1
>>> n1 = n2
>>> n2 = temp
>>>print('n1-> ', n1, 'n2-> ', n2)
n1->200 n2->100
>>>

In Python, if you have two tuples say t1 and t2, then you can change them as follows:

In [2]:
t1, t2 = (1, 'one'), (2, 'two')
t1, t2 = t2, t1
print('t1->', t1, 't2->', t2)
t3, t4 = t1  # t1 is 
print('t3->', t3, 't4->', t4)
t1-> (2, 'two') t2-> (1, 'one')
t3-> 2 t4-> two

10.3.4. Unpacking tuples
Tuples have an interesting property, which is best demonstrated by the following example:

# ---ON IDLE---
>>> t1 = ('one', 'two', 'three')
>>> x, y, z = t1 # Tuple t1 unpacked and its 3 items assigned to x, y and z
>>> x
'one'
>>> t1[0] 
'one'
>>> y
'two'

You can use this to swap values of variables in Python (As done above). A more general case is shown as follows:

# ---ON IDLE---
>>> x, y, z = ('one', 'two', 'three')
>>> x, y, z
('one', 'two', 'three')
>>> x, y, z = y, z, x # swap leads to y->x, z->y, x->z
>>> x, y, z
('two', 'three', 'one')

10.6. Beyond text book
See Page 236 of the book

  1. Python has a module fractions for rational number arithmetic. In many situations you may like to represent a number as 1/2 rather than 0.5. This is where the fractions module is useful. The module has a method Fraction whose two possible signatures are shown as follows:
class fractions.Fraction(numerator=0, denominator=1)
class fractions.Fraction(string)
# string should be of type:- [sign] numerator ['/' denominator]

In the second case, the string must be of type [sign] numerator ['/' denominator]
Examples:

from fractions import Fraction
print(Fraction(2, -6)) # -1/3
print('3/9') #  1/3

2. Python provides built-in support for basic data structures, such as strings, lists, tuples, and so on. However, it also has a module called collections.
The collections module provides alternative containers apart from the built-in containers, such as lists, tuples and dictionaries. The task is to explore the collection module. An example of how the Counter class of the collections module is used as follows:

import collections
# Take of list of words
word_list = ['car', 'bus', 'truck', 'car', 'car']
count_words = collections.Counter(word_list)
print(count_words)
# You can cast the Counter class object to dictionary
print(dict(count_words))

3. The collections module mentioned also has a class to create a deque object.
Deque pronounced deck stands for double ended queue. The deque module provides append and pop from either end of the queue. Write scripts, which use various methods of the deque object, such as pop(), reverse(), rotate(), popleft(), and so on. Hint:- Example code to clarify the concepts as follows:

In [3]:
from collections import deque
# Create
d = deque('abcdefgh')
print(type(d))  # <class 'collections.deque'>
print(d)  # deque(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])
item1 = d.popleft()
print(item1)  # a
# Rotate right by 3
d.rotate(3)  
print(d)  # deque(['f', 'g', 'h', 'b', 'c', 'd', 'e'])
# Now rotate left using -3
d.rotate(-3)  
print(d)  # deque(['b', 'c', 'd', 'e', 'f', 'g', 'h'])
<class 'collections.deque'>
deque(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])
a
deque(['f', 'g', 'h', 'b', 'c', 'd', 'e'])
deque(['b', 'c', 'd', 'e', 'f', 'g', 'h'])

4. Understanding and using namedtuple of collections module
The collections module has another useful container called namedtuple, which can be created from the collections.namedtuple() function.
Before using named tuples, you need to understand the concept, which is best understood by an example. Suppose you want to make a table of students with three records (that is, three students) and three attributes or fields, that is, name, age and sex. This is shown as follows:

Name Age Sex
Anil 20 M
Sunita 22 F
Radha 21 F

You could represent each student using a tuple, such as say Anil as (‘Anil’, 20, ‘M’). But such a tuple has the record entries, but does not have the field entries.
Another way could be to use a dictionary, which has the field names as keys and the records as values. So, you could represent Radha as {‘Name’: ‘Radha’, ‘Age’: 21, ‘Sex’: ‘F’}. But the collections module provides a better way of doing this, that is, by using named tuples.
There are many ways to create named tuples, but the following discussion considers only one such method.
The constructor of a named tuple is namedtuple(param1, param2), where param1 is a Python string representing the name of the named tuple and param2 is a white space separated string, where each word in the string represents one field name.
The concept will be clear after studying the following example:

In [4]:
from collections import namedtuple
# param1 is a string giving "name" of the named tuple
param1 = 'Student'
# param2 is a string with each field seperated by space
param2 = 'Name Age Sex'
Student = namedtuple(param1, param2)
s1 = Student('Anil', 20, 'M')
s2 = Student('Sunita', 22, 'F')
s3 = Student('Radha', 21, 'F')
# print s1
print(s1)  # Student(Name='Radha', Age=21, Sex='F')
# You can use index or field name
print('s1 name->',s1[0], 's1 age->', s1.Age)
Student(Name='Anil', Age=20, Sex='M')
s1 name-> Anil s1 age-> 20

Another thing to note about named tuples is that it has a number of helper methods and many of them start with an underscore (_).
As you will read later, an underscore for attribute names is used in Python to indicate private variables, but this is not the case here. In case of helper, one such helper method is _asdict(), which can be used to convert a named tuple into a Python dictionary.
This is shown as follows:

In [5]:
from collections import namedtuple
# param1 is a string giving "name" of the named tuple
param1 = 'Student'
# param2 is a string with each field seperated by space
param2 = 'Name Age Sex'
Student = namedtuple(param1, param2)
s1 = Student('Anil', 20, 'M')
d1 = s1._asdict()
print(d1)
# d1 can be "cast" to a normal python dictionary
d2 = dict(d1)
print(d2)
OrderedDict([('Name', 'Anil'), ('Age', 20), ('Sex', 'M')])
{'Name': 'Anil', 'Age': 20, 'Sex': 'M'}

10.7 Assignment
See Page 239 of the book
1. Understanding and using the factory function namedtuple() of the collections module
Nowadays, the source code of most of the modules of Python is available on github. Studying source code of Python modules is an excellent way of learning Python. The current assignment is to study the source code of the namedtuple() factory function in the collections module.
The steps are as follows:

  • Locate the source code file __init__.py of the collections module. You can find this file at two places, that is, either on the internet or also on your local machine.
  • The definition of the namedtuple() function begins around line 319. (You can always do a search to locate this function.)
  • The doc string of this function also contains many example codes. In fact, many Python libraries include example code in their docstrings.

The signature of namedtuples() function of collections module is as follows (Output truncated to show only relevant portion):

def namedtuple(typename, field_names, 
               *, rename=False, defaults=None, module=None):
    Returns a new subclass of tuple with named fields.
Parameters (Only first 2 are discussed):-
(1) typename:- It is the name of the class (Which is a sub-class of Python class tuple ie tup) which is returned by the factory function.
(2) field_names:-This parameter can be a string consisting of the “names” of the fields. The names given in the string can be separated by “space” or by “a comma”. The field names can also be a “list of field names where each individual name in the list is a string”
Return value:
This factory function returns a class whose name is the first parameter given to the function

Some points to note are as follows:

  • namedtuples() is what is called a factory function. This means that even though it is a function, it creates a class. You can make out that it is a function (and not a class) by seeing its name which does not begin with a capital letter.
  • The namedtuple() function returns a class (Which is a subclass of the tuples class of Python). The name of this subclass is the first parameter given to the function. The following example will clarify the concept: Consider the following scenario:
  • For some script you need 3-D points, such as (x, y, z).
  • You can have a Point class
  • The data of the Point class should be immutable The following sample script shows how to use namedtuple() factory function to create this Point class:
In [6]:
from collections import namedtuple
# Create a class Point which inherits from class tup ie tuples
# Name of this sub-class is Points
# This class ie Point can take 3 items named x, y and z
Point = namedtuple(typename = 'Point', field_names = 'x y z')
# docstring for this class Point is automatically created
print('docstring of Point class->', Point.__doc__)
# Create some Point objects
p1 = Point(1, 2, 3)
print('type of p1->', type(p1))  # p1 is of type Point
print('p1->', p1)
# Can use index like for a normal tuple also
print('item at index 2 of p1->', p1[2])
docstring of Point class-> Point(x, y, z)
type of p1-> <class '__main__.Point'>
p1-> Point(x=1, y=2, z=3)
item at index 2 of p1-> 3

2. You have studied various types of containers (and sequences). You have also studied various operations, such as addition, or finding say gcd of two numbers. But suppose you have to find the sum of all numbers in a container, say, a list. The hard way would be to create a loop and iterate over each member of the container and perform the necessary operation. But Python provides a tool for this. This tool is called reduce() and is available in the functools module . The signature of the function is somewhat as follows:

Docstring: reduce(function, sequence[, initial]) -> value
Apply a function of two arguments cumulatively to the items of a sequence,
from left to right, so as to reduce the sequence to a single value.
For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates
((((1+2)+3)+4)+5).

ASSIGNMENT 1:
Write a script which generates three random numbers in range (0, 100) and then uses the reduce() method of functools module to get (i) gcd (ii) sum of numbers in the list.

In [7]:
from math import gcd
from functools import reduce
import random
# Create a list of 3 random integers
list_rand = [random.randint(0, 100) for x in range(3)]
print('list random numbers->', list_rand)
# Get gcd of items in list
list_gcd = reduce(gcd, list_rand)
print('gcd of numbers in list->',list_gcd)
# Get sum of items in list
list_add = reduce(lambda x,y: x+y, list_rand)
print('sum of numbers in list->', list_add)
list random numbers-> [91, 8, 34]
gcd of numbers in list-> 1
sum of numbers in list-> 133

ASSIGNMENT 2:
It is a well-known fact that gcd(a*n, b*n, c*n . . .) is n. This simple but not so obvious fact is often used in some attacks to guess random numbers.
Write a script, which shows that gcd(a*n, b*n, c*n . . .) is n.
Solution:

In [8]:
# Script shows that modulo can be got from gcd of its random multiples
from math import gcd
from functools import reduce
from random import randint
# m is the modulo
m = 2 ** 31 - 1
# see value of m
print(m)
# Generate 3 multiples of m
rand_multiples = [randint(1, 1000000) * m for x in range(3)]
# print the 3 multiples of m
print(rand_multiples)
# get gcd of these 3 random multiples of m
rand_gcd = reduce(gcd, rand_multiples)
print(rand_gcd)
2147483647
[1646031183039971, 723888820116289, 288014064284699]
2147483647